/**
 * 
 */
package org.swing.utility.common.codec;

import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;

/**
 * @author lqnhu
 *
 */
public class URLEncoder implements Appendable, Flushable, Closeable {
	protected static final int DEFAULT_BUFFER_SIZE = 1 << 10;
	protected final Appendable out;
	protected final Charset charset;
	protected final CharBuffer singleCharBuffer = CharBuffer.allocate(1);
	protected final CharsetEncoder ce;
	protected final CharBuffer cb;
	protected final ByteBuffer bb;

	/**
	 * Encodes to the specified {@link Appendable}, using
	 * {@link #DEFAULT_BUFFER_SIZE} and {@link URLCodec#UTF_8}. Convenience
	 * method for {@link #URLEncoder(Appendable, int, Charset)}.
	 */
	public URLEncoder(Appendable out) {
		this(out, DEFAULT_BUFFER_SIZE, URLCodec.UTF_8);
	}

	/**
	 * Encodes to the specified {@link Appendable} with the specified
	 * bufferSize, using {@link URLCodec#UTF_8}. Convenience method for
	 * {@link #URLEncoder(Appendable, int, Charset)}.
	 */
	public URLEncoder(Appendable out, int bufferSize) {
		this(out, bufferSize, URLCodec.UTF_8);
	}

	/**
	 * Encodes to the specified {@link Appendable} with the specified bufferSize
	 * and charset.
	 */
	public URLEncoder(Appendable out, int bufferSize, Charset charset) {
		this.out = out;
		this.charset = charset;
		this.ce = charset.newEncoder()
				.onMalformedInput(CodingErrorAction.REPLACE)
				.onUnmappableCharacter(CodingErrorAction.REPLACE);
		this.cb = CharBuffer.allocate(bufferSize);
		this.bb = ByteBuffer.allocate((int) Math.ceil(bufferSize
				* ce.maxBytesPerChar()));
	}

	public Appendable append(CharSequence in) throws IOException {
		for (int start = 0, len = in.length(); start < len;) {
			int read = Math.min(cb.remaining(), len - start);
			cb.append(in, start, start + read);
			start += read;
			if (!cb.hasRemaining()) {
				flush(false);
			}
		}
		return this;
	}

	public Appendable append(char c) throws IOException {
		singleCharBuffer.put(0, c);
		return append(singleCharBuffer);
	}

	public Appendable append(CharSequence csq, int start, int end)
			throws IOException {
		return append(csq.subSequence(start, end));
	}

	protected void flush(boolean endOfInput) throws IOException {
		cb.flip();
		CoderResult cr = ce.encode(cb, bb, endOfInput);
		if (cr.isUnderflow()) {
			bb.flip();
			URLCodec.encode(bb, out);
		} else {
			throw new IOException("Unexpected: " + cr);
		}
		cb.clear();
		if (endOfInput) {
			cr = ce.flush(bb);
			if (cr.isUnderflow()) {
				bb.flip();
				URLCodec.encode(bb, out);
			} else {
				throw new IOException("Unexpected: " + cr);
			}
			ce.reset();
		}
	}

	public void flush() throws IOException {
		flush(true);
	}

	public void close() throws IOException {
		flush();
	}
}
